package weka.filters.unsupervised.instance;

import java.util.Enumeration;

import weka.core.OptionHandler;
import weka.core.Capabilities;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.RevisionUtils;
import weka.core.Utils;
import weka.core.Capabilities.Capability;
import weka.filters.Filter;
import weka.filters.SupervisedFilter;
import weka.filters.supervised.instance.Resample;

import java.util.Collections;
import java.util.Enumeration;
import java.util.Random;
import java.util.Vector;

/**
 * My Random over sampling method.
 * 
 * @author huicongwm
 * 
 */
public class _HC_ROS extends Filter implements OptionHandler {

	/**
	 * GUIEditors.props
	 */

	/**
	 * The percent of dataset to be multiple
	 */
	protected double m_SampleSizePercent = 100;

	protected int m_RandomSeed = 1;

	public String globalInfo() {
		return "Over sampling the smallest class by percent";
	}

	public String[] getOptions() {
		Vector<String> result;

		result = new Vector<String>();

		result.add("-S");
		result.add("" + getRandomSeed());

		result.add("-Z");
		result.add("" + getSampleSizePercent());

		return result.toArray(new String[result.size()]);
	}

	public Enumeration listOptions() {
		Vector result = new Vector();
		result.add(new Option("The size of dataset by percent.", "Z", 1,
				"-Z <num>"));
		result.add(new Option("Random size", "S", 1, "-S <num>"));
		return result.elements();
	}

	/**
	 * Gets the random number seed.
	 * 
	 * @return the random number seed.
	 */
	public int getRandomSeed() {
		return m_RandomSeed;
	}

	/**
	 * Sets the random number seed.
	 * 
	 * @param newSeed
	 *            the new random number seed.
	 */
	public void setRandomSeed(int newSeed) {
		m_RandomSeed = newSeed;
	}

	/**
	 * Returns the tip text for this property.
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String sampleSizePercentTipText() {
		return "The subsample size as a percentage of the original set.";
	}

	/**
	 * Gets the subsample size as a percentage of the original set.
	 * 
	 * @return the subsample size
	 */
	public double getSampleSizePercent() {
		return m_SampleSizePercent;
	}

	/**
	 * Sets the size of the subsample, as a percentage of the original set.
	 * 
	 * @param newSampleSizePercent
	 *            the subsample set size, between 0 and 100.
	 */
	public void setSampleSizePercent(double newSampleSizePercent) {
		m_SampleSizePercent = newSampleSizePercent;
	}

	public void setOptions(String[] options) throws Exception {
		String tmpStr;

		tmpStr = Utils.getOption('S', options);
		if (tmpStr.length() != 0)
			setRandomSeed(Integer.parseInt(tmpStr));
		else
			setRandomSeed(1);

		tmpStr = Utils.getOption('Z', options);
		if (tmpStr.length() != 0)
			setSampleSizePercent(Double.parseDouble(tmpStr));
		else
			setSampleSizePercent(100);

		if (getInputFormat() != null) {
			setInputFormat(getInputFormat());
		}

	}

	/**
	 * Returns the Capabilities of this filter.
	 * 
	 * @return the capabilities of this object
	 * @see Capabilities
	 */
	public Capabilities getCapabilities() {
		Capabilities result = super.getCapabilities();

		// attributes
		result.enableAllAttributes();
		result.enable(Capability.MISSING_VALUES);

		// class
		result.enable(Capability.NOMINAL_CLASS);

		return result;
	}

	/**
	 * Sets the format of the input instances.
	 * 
	 * @param instanceInfo
	 *            an Instances object containing the input instance structure
	 *            (any instances contained in the object are ignored - only the
	 *            structure is required).
	 * @return true if the outputFormat may be collected immediately
	 * @throws Exception
	 *             if the input format can't be set successfully
	 */
	public boolean setInputFormat(Instances instanceInfo) throws Exception {

		super.setInputFormat(instanceInfo);
		setOutputFormat(instanceInfo);
		return true;
	}

	/**
	 * Input an instance for filtering. Filter requires all training instances
	 * be read before producing output.
	 * 
	 * @param instance
	 *            the input instance
	 * @return true if the filtered instance may now be collected with output().
	 * @throws IllegalStateException
	 *             if no input structure has been defined
	 */
	public boolean input(Instance instance) {

		if (getInputFormat() == null) {
			throw new IllegalStateException("No input instance format defined");
		}
		if (m_NewBatch) {
			resetQueue();
			m_NewBatch = false;
		}
		if (isFirstBatchDone()) {
			push(instance);
			return true;
		} else {
			bufferInput(instance);
			return false;
		}
	}

	/**
	 * Signify that this batch of input to the filter is finished. If the filter
	 * requires all instances prior to filtering, output() may now be called to
	 * retrieve the filtered instances.
	 * 
	 * @return true if there are instances pending output
	 * @throws IllegalStateException
	 *             if no input structure has been defined
	 */
	public boolean batchFinished() {

		if (getInputFormat() == null) {
			throw new IllegalStateException("No input instance format defined");
		}

		if (!isFirstBatchDone()) {
			// Do the subsample, and clear the input instances.
			createSubsample();
		}
		flushInput();

		m_NewBatch = true;
		m_FirstBatchDone = true;
		return (numPendingOutput() != 0);
	}

	/**
	 * Creates a subsample of the current set of input instances. The output
	 * instances are pushed onto the output queue for collection.
	 */
	protected void createSubsample() {
		int origSize = getInputFormat().numInstances();

		// Subsample that takes class distribution into consideration

		// Sort according to class attribute.
		getInputFormat().sort(getInputFormat().classIndex());

		// Create an index of where each class value starts
		int[] classIndices = new int[getInputFormat().numClasses() + 1];
		int currentClass = 0;
		classIndices[currentClass] = 0;
		for (int i = 0; i < getInputFormat().numInstances(); i++) {
			Instance current = getInputFormat().instance(i);
			if (current.classIsMissing()) {
				for (int j = currentClass + 1; j < classIndices.length; j++) {
					classIndices[j] = i;
				}
				break;
			} else if (current.classValue() != currentClass) {
				for (int j = currentClass + 1; j <= current.classValue(); j++) {
					classIndices[j] = i;
				}
				currentClass = (int) current.classValue();
			}
		}
		if (currentClass <= getInputFormat().numClasses()) {
			for (int j = currentClass + 1; j < classIndices.length; j++) {
				classIndices[j] = getInputFormat().numInstances();
			}
		}

		int actualClasses = 0;
		for (int i = 0; i < classIndices.length - 1; i++) {
			if (classIndices[i] != classIndices[i + 1]) {
				actualClasses++;
			}
		}

		int smallClassIndex = 0;
		int smallClassNum = getInputFormat().numInstances();
		for (int i = 0; i < classIndices.length - 1; i++) {
			if (classIndices[i + 1] - classIndices[i] < smallClassNum) {
				smallClassIndex = i;
				smallClassNum = classIndices[i + 1] - classIndices[i];
			}
		}

		Random random = new Random(m_RandomSeed);

		int sampleSize = (int) (smallClassNum * m_SampleSizePercent / 100);
		for (int i = 0; i < getInputFormat().numInstances(); ++i) {
			push((Instance) getInputFormat().instance(i).copy());
		}
		for (int i = 0; i < sampleSize; ++i) {
			int offset = random.nextInt(smallClassNum);
			push((Instance) getInputFormat().instance(
					classIndices[smallClassIndex] + offset).copy());
		}
	}

	/**
	 * Returns the revision string.
	 * 
	 * @return the revision
	 */
	public String getRevision() {
		return RevisionUtils.extract("$Revision: 1.13 $");
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		runFilter(new Resample(), args);
	}

}
